resize&drag&snap scriptのasync-await書き換えテスト
interface構想
code:ts
// Resize & Drag & Snap機能を付与
const detach = attachRDS(dom, ghostDom, {
// There are default values
resize: {
minWidth: 60,
minHeight: 40,
},
margins: {
normal: -10,
fullscreen: 4,
},
});
// ...
// 機能解除
detach();
code:mod.ts
/*
*/
import { setBounds } from "./setBounds.ts";
import {
getPromiseSettledAnytimes
} from "../getPromiseSettledAnytimes/mod.ts";
export interface Options {
resize: {
minWidth: number;
minHeigth: number;
},
margins: {
normal: number;
fullscreen: number;
};
};
export const attachRDS: (
dom: HTMLElement,
ghost: HTMLElement,
options?: Options,
) => (() => void) = (dom, ghost, { resize, margins } = {}) => {
const {
minWidth = 60,
minHeight = 40,
} = resize ?? {};
const {
normal = -10,
fullscreen = 4,
} = margin ?? {};
let preSnapped: {
width: number;
height: number;
} | undefined;
let clicked;
const onMove = (e: MouseEvent | Touch) => {
requestAnimationFrame(
animate(
{ e, ...calc(e, dom, normal) },
clicked,
preSnapped
)
);
};
MouseEvent | Touch
();
MouseEvent | Touch
();
const handleMouseDown = (e: MouseEvent) => {
downListener(e);
e.preventDefault();
};
const handleTouchStart = (e: TouchEvent) => {
downListener(e.touches0); e.preventDefault();
};
const handleTouchMove = (e) => {
};
const handleTouchEnd = (e: TouchEvent) => {
// すべてのpointersが画面から離れたら終了とみなす
if (e.touches.length === 0) upListener(e.changedTouches0); };
dom.addEventListener("mousedown", handleMouseDown);
dom.addEventListener("mousemove", onMove);
dom.addEventListener("mouseup", upListener);
dom.addEventListener("touchstart", handleTouchStart);
dom.addEventListener("touchmove", handleTouchMove);
dom.addEventListener("touchend", handleTouchEnd);
let finished = false;
const cleanup = () => {
finished = true;
dom.removeEventListener("mousedown", handleMouseDown);
dom.removeEventListener("mousemove", onMove);
dom.removeEventListener("mouseup", upListener);
dom.removeEventListener("touchmove", handleTouchMove);
dom.removeEventListener("touchstart", handleTouchStart);
dom.removeEventListener("touchend", handleTouchEnd);
};
let state: "drag" | "resize" | "neutral" = "neutral";
(async () => {
while (!finished) {
const e = await waitDown();
const {
x, y, b,
...rest
} = calc(e, dom, normal);
const isResizing = rest.onRightEdge ||
rest.onBottomEdge ||
rest.onTopEdge ||
rest.onLeftEdge;
state = isResizing ? "resize" :
canMove(x, y, b) ? "drag" :
"neutral";
clicked = {
cx: e.clientX,
cy: e.clientY,
w: b.width,
h: b.height,
x,
y,
...rest
};
const { b: snapped } = calc(await waitUp(), dom, normal);
// Snap
if (state === "drag") {
const b = snapped;
if (
b.top < fullscreen ||
b.left < fullscreen ||
b.right > window.innerWidth - fullscreen ||
b.bottom > window.innerHeight - fullscreen
) {
// hintFull();
setBounds(dom, 0, 0, window.innerWidth, window.innerHeight);
preSnapped = snapped;
} else if (b.top < normal) {
// hintTop();
setBounds(dom, 0, 0, window.innerWidth, window.innerHeight / 2);
preSnapped = snapped;
} else if (b.left < normal) {
// hintLeft();
setBounds(dom, 0, 0, window.innerWidth / 2, window.innerHeight);
preSnapped = snapped;
} else if (b.right > rightScreenEdge) {
// hintRight();
setBounds(dom, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight);
preSnapped = snapped;
} else if (b.bottom > bottomScreenEdge) {
// hintBottom();
setBounds(dom, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2);
preSnapped = snapped;
} else {
preSnapped = null;
}
hintHide(ghost, snapped);
}
}
})();
return cleanup;
}
function hintHide(ghostpane: HTMLElement, b: DOMRect) {
setBounds(ghostpane, b.left, b.top, b.width, b.height);
ghostpane.style.opacity = 0;
}
let data: {
e: MouseEvent | Touch;
b: DOMRect;
x: number;
y: number;
/** pointerがコンテナの端にあるかどうか */
onRightEdge: boolean;
onBottomEdge: boolean;
onLeftEdge: boolean;
onTopEdge: boolean;
/** 画面の端との距離 */
rightScreenEdge: number;
bottomScreenEdge: number;
};
let clicked: {
x: number;
y: number;
cx: number;
cy: number;
w: number;
h: number;
state: "resize" | "drag" | "neutral";
onLeftEdge: boolean;
onTopEdge: boolean;
onRightEdge: boolean;
onBottomEdge: boolean;
} | { state: "neutral"; } = { state: "neutral" };
const canMove = (x: number, y: number, b) =>
x > 0 && x < b.width && y > 0 && y < b.height && y < 30;
function calc(e: MouseEvent | Touch, dom: HTMLElement, margin: number) {
const b = dom.getBoundingClientRect();
const x = e.clientX - b.left;
const y = e.clientY - b.top;
return {
b,
x,
y,
onTopEdge: y < margin,
onLeftEdge: x < margin,
onRightEdge: x >= b.width - margin,
onBottomEdge: y >= b.height - margin,
rightScreenEdge: window.innerWidth - margin,
bottomScreenEdge: window.innerHeight - margin,
};
}
let preSnapped: {
width: number;
height: number;
} | null;
function animate(data, clicked, preSnapped) {
const { x, y, b, e } = data;
switch (state) {
case "resize": {
if (clicked.onRightEdge) pane.style.width = `${
Math.max(x, minWidth)
}px`;
if (clicked.onBottomEdge) pane.style.height = `${
Math.max(y, minHeight)
}px`;
if (clicked.onLeftEdge) {
const currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth);
if (currentWidth > minWidth) {
pane.style.width = ${currentWidth}px;
pane.style.left = ${e.clientX}px;
}
}
if (clicked.onTopEdge) {
const currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight);
if (currentHeight > minHeight) {
pane.style.height = ${currentHeight}px;
pane.style.top = ${e.clientY}px;
}
}
hintHide(b);
return;
}
case "drag": {
if (b.top < FULLSCREEN_MARGINS ||
b.left < FULLSCREEN_MARGINS ||
b.right > window.innerWidth - FULLSCREEN_MARGINS ||
b.bottom > window.innerHeight - FULLSCREEN_MARGINS
) {
// hintFull();
setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.top < MARGINS) {
// hintTop();
setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight / 2);
ghostpane.style.opacity = 0.2;
} else if (b.left < MARGINS) {
// hintLeft();
setBounds(ghostpane, 0, 0, window.innerWidth / 2, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.right > rightScreenEdge) {
// hintRight();
setBounds(ghostpane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.bottom > bottomScreenEdge) {
// hintBottom();
setBounds(ghostpane, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2);
ghostpane.style.opacity = 0.2;
} else {
hintHide(b);
}
// 前回スナップしていた場合は移動量計算を変える
if (preSnapped) {
setBounds(pane,
e.clientX - preSnapped.width / 2,
e.clientY - Math.min(clicked.y, preSnapped.height),
preSnapped.width,
preSnapped.height
);
return;
}
// moving
pane.style.top = ${e.clientY - clicked.y}px;
pane.style.left = ${e.clientX - clicked.x}px;
return;
}
}
// This code executes when mouse moves without clicking
// style cursor
const {
onLeftEdge,
onTopEdge,
onRightEdge,
onBottomEdge,
} = data;
pane.style.cursor =
onRightEdge && onBottomEdge || onLeftEdge && onTopEdge ?
"nwse-resize" :
onRightEdge && onTopEdge || onBottomEdge && onLeftEdge ?
"nesw-resize" :
onRightEdge || onLeftEdge ?
"ew-resize" :
onBottomEdge || onTopEdge ?
"ns-resize" :
canMove(x, y, b) ?
"move" :
"default";
}